home *** CD-ROM | disk | FTP | other *** search
/ EuroCD 3 / EuroCD 3.iso / Programming / Python-1.4 / Lib / mimify.py < prev    next >
Text File  |  1998-06-24  |  11KB  |  425 lines

  1. #!/usr/local/bin/python
  2.  
  3. '''Mimification and unmimification of mail messages.
  4.  
  5. decode quoted-printable parts of a mail message or encode using
  6. quoted-printable.
  7.  
  8. Usage:
  9.     mimify(input, output)
  10.     unmimify(input, output)
  11. to encode and decode respectively.  Input and output may be the name
  12. of a file or an open file object.  Only a readline() method is used
  13. on the input file, only a write() method is used on the output file.
  14. When using file names, the input and output file names may be the
  15. same.
  16.  
  17. Interactive usage:
  18.     mimify.py -e [infile [outfile]]
  19.     mimify.py -d [infile [outfile]]
  20. to encode and decode respectively.  Infile defaults to standard
  21. input and outfile to standard output.
  22. '''
  23.  
  24. # Configure
  25. MAXLEN = 200    # if lines longer than this, encode as quoted-printable
  26. CHARSET = 'ISO-8859-1'    # default charset for non-US-ASCII mail
  27. QUOTE = '> '        # string replies are quoted with
  28. # End configure
  29.  
  30. import regex, regsub, string
  31.  
  32. qp = regex.compile('^content-transfer-encoding:[ \t]*quoted-printable',
  33.            regex.casefold)
  34. base64 = regex.compile('^content-transfer-encoding:[ \t]*base64',
  35.                regex.casefold)
  36. mp = regex.compile('^content-type:[\000-\377]*multipart/[\000-\377]*boundary="?\\([^;"\n]*\\)',
  37.            regex.casefold)
  38. chrset = regex.compile('^\\(content-type:.*charset="\\)\\(us-ascii\\|iso-8859-[0-9]+\\)\\("[\000-\377]*\\)',
  39.                regex.casefold)
  40. he = regex.compile('^-*$')
  41. mime_code = regex.compile('=\\([0-9a-f][0-9a-f]\\)', regex.casefold)
  42. mime_head = regex.compile('=\\?iso-8859-1\\?q\\?\\([^?]+\\)\\?=',
  43.               regex.casefold)
  44. repl = regex.compile('^subject:[ \t]+re: ', regex.casefold)
  45.  
  46. class File:
  47.     '''A simple fake file object that knows about limited
  48.        read-ahead and boundaries.
  49.        The only supported method is readline().'''
  50.  
  51.     def __init__(self, file, boundary):
  52.         self.file = file
  53.         self.boundary = boundary
  54.         self.peek = None
  55.  
  56.     def readline(self):
  57.         if self.peek is not None:
  58.             return ''
  59.         line = self.file.readline()
  60.         if not line:
  61.             return line
  62.         if self.boundary:
  63.             if line == self.boundary + '\n':
  64.                 self.peek = line
  65.                 return ''
  66.             if line == self.boundary + '--\n':
  67.                 self.peek = line
  68.                 return ''
  69.         return line
  70.  
  71. class HeaderFile:
  72.     def __init__(self, file):
  73.         self.file = file
  74.         self.peek = None
  75.  
  76.     def readline(self):
  77.         if self.peek is not None:
  78.             line = self.peek
  79.             self.peek = None
  80.         else:
  81.             line = self.file.readline()
  82.         if not line:
  83.             return line
  84.         if he.match(line) >= 0:
  85.             return line
  86.         while 1:
  87.             self.peek = self.file.readline()
  88.             if len(self.peek) == 0 or \
  89.                (self.peek[0] != ' ' and self.peek[0] != '\t'):
  90.                 return line
  91.             line = line + self.peek
  92.             self.peek = None
  93.  
  94. def mime_decode(line):
  95.     '''Decode a single line of quoted-printable text to 8bit.'''
  96.     newline = ''
  97.     while 1:
  98.         i = mime_code.search(line)
  99.         if i < 0:
  100.             break
  101.         newline = newline + line[:i] + \
  102.               chr(string.atoi(mime_code.group(1), 16))
  103.         line = line[i+3:]
  104.     return newline + line
  105.  
  106. def mime_decode_header(line):
  107.     '''Decode a header line to 8bit.'''
  108.     newline = ''
  109.     while 1:
  110.         i = mime_head.search(line)
  111.         if i < 0:
  112.             break
  113.         match = mime_head.group(0, 1)
  114.         newline = newline + line[:i] + mime_decode(match[1])
  115.         line = line[i + len(match[0]):]
  116.     return newline + line
  117.  
  118. def unmimify_part(ifile, ofile):
  119.     '''Convert a quoted-printable part of a MIME mail message to 8bit.'''
  120.     multipart = None
  121.     quoted_printable = 0
  122.     is_repl = 0
  123.     if ifile.boundary and ifile.boundary[:2] == QUOTE:
  124.         prefix = QUOTE
  125.     else:
  126.         prefix = ''
  127.  
  128.     # read header
  129.     hfile = HeaderFile(ifile)
  130.     while 1:
  131.         line = hfile.readline()
  132.         if not line:
  133.             return
  134.         if prefix and line[:len(prefix)] == prefix:
  135.             line = line[len(prefix):]
  136.             pref = prefix
  137.         else:
  138.             pref = ''
  139.         line = mime_decode_header(line)
  140.         if qp.match(line) >= 0:
  141.             quoted_printable = 1
  142.             continue    # skip this header
  143.         ofile.write(pref + line)
  144.         if not prefix and repl.match(line) >= 0:
  145.             # we're dealing with a reply message
  146.             is_repl = 1
  147.         if mp.match(line) >= 0:
  148.             multipart = '--' + mp.group(1)
  149.         if he.match(line) >= 0:
  150.             break
  151.     if is_repl and (quoted_printable or multipart):
  152.         is_repl = 0
  153.  
  154.     # read body
  155.     while 1:
  156.         line = ifile.readline()
  157.         if not line:
  158.             return
  159.         line = regsub.gsub(mime_head, '\\1', line)
  160.         if prefix and line[:len(prefix)] == prefix:
  161.             line = line[len(prefix):]
  162.             pref = prefix
  163.         else:
  164.             pref = ''
  165. ##        if is_repl and len(line) >= 4 and line[:4] == QUOTE+'--' and line[-3:] != '--\n':
  166. ##            multipart = line[:-1]
  167.         while multipart:
  168.             if line == multipart + '--\n':
  169.                 ofile.write(pref + line)
  170.                 multipart = None
  171.                 line = None
  172.                 break
  173.             if line == multipart + '\n':
  174.                 ofile.write(pref + line)
  175.                 nifile = File(ifile, multipart)
  176.                 unmimify_part(nifile, ofile)
  177.                 line = nifile.peek
  178.                 continue
  179.             # not a boundary between parts
  180.             break
  181.         if line and quoted_printable:
  182.             while line[-2:] == '=\n':
  183.                 line = line[:-2]
  184.                 newline = ifile.readline()
  185.                 if newline[:len(QUOTE)] == QUOTE:
  186.                     newline = newline[len(QUOTE):]
  187.                 line = line + newline
  188.             line = mime_decode(line)
  189.         if line:
  190.             ofile.write(pref + line)
  191.  
  192. def unmimify(infile, outfile):
  193.     '''Convert quoted-printable parts of a MIME mail message to 8bit.'''
  194.     if type(infile) == type(''):
  195.         ifile = open(infile)
  196.         if type(outfile) == type('') and infile == outfile:
  197.             import os
  198.             d, f = os.path.split(infile)
  199.             os.rename(infile, os.path.join(d, ',' + f))
  200.     else:
  201.         ifile = infile
  202.     if type(outfile) == type(''):
  203.         ofile = open(outfile, 'w')
  204.     else:
  205.         ofile = outfile
  206.     nifile = File(ifile, None)
  207.     unmimify_part(nifile, ofile)
  208.     ofile.flush()
  209.  
  210. mime_char = regex.compile('[=\240-\377]') # quote these chars in body
  211. mime_header_char = regex.compile('[=?\240-\377]') # quote these in header
  212.  
  213. def mime_encode(line, header):
  214.     '''Code a single line as quoted-printable.
  215.        If header is set, quote some extra characters.'''
  216.     if header:
  217.         reg = mime_header_char
  218.     else:
  219.         reg = mime_char
  220.     newline = ''
  221.     if len(line) >= 5 and line[:5] == 'From ':
  222.         # quote 'From ' at the start of a line for stupid mailers
  223.         newline = string.upper('=%02x' % ord('F'))
  224.         line = line[1:]
  225.     while 1:
  226.         i = reg.search(line)
  227.         if i < 0:
  228.             break
  229.         newline = newline + line[:i] + \
  230.               string.upper('=%02x' % ord(line[i]))
  231.         line = line[i+1:]
  232.     line = newline + line
  233.  
  234.     newline = ''
  235.     while len(line) >= 75:
  236.         i = 73
  237.         while line[i] == '=' or line[i-1] == '=':
  238.             i = i - 1
  239.         i = i + 1
  240.         newline = newline + line[:i] + '=\n'
  241.         line = line[i:]
  242.     return newline + line
  243.  
  244. mime_header = regex.compile('\\([ \t(]\\|^\\)\\([-a-zA-Z0-9_+]*[\240-\377][-a-zA-Z0-9_+\240-\377]*\\)\\([ \t)]\\|$\\)')
  245.  
  246. def mime_encode_header(line):
  247.     '''Code a single header line as quoted-printable.'''
  248.     newline = ''
  249.     while 1:
  250.         i = mime_header.search(line)
  251.         if i < 0:
  252.             break
  253.         newline = newline + line[:i] + mime_header.group(1) + \
  254.               '=?' + CHARSET + '?Q?' + \
  255.               mime_encode(mime_header.group(2), 1) + \
  256.               '?=' + mime_header.group(3)
  257.         line = line[i+len(mime_header.group(0)):]
  258.     return newline + line
  259.  
  260. mv = regex.compile('^mime-version:', regex.casefold)
  261. cte = regex.compile('^content-transfer-encoding:', regex.casefold)
  262. iso_char = regex.compile('[\240-\377]')
  263.  
  264. def mimify_part(ifile, ofile, is_mime):
  265.     '''Convert an 8bit part of a MIME mail message to quoted-printable.'''
  266.     has_cte = is_qp = is_base64 = 0
  267.     multipart = None
  268.     must_quote_body = must_quote_header = has_iso_chars = 0
  269.  
  270.     header = []
  271.     header_end = ''
  272.     message = []
  273.     message_end = ''
  274.     # read header
  275.     hfile = HeaderFile(ifile)
  276.     while 1:
  277.         line = hfile.readline()
  278.         if not line:
  279.             break
  280.         if not must_quote_header and iso_char.search(line) >= 0:
  281.             must_quote_header = 1
  282.         if mv.match(line) >= 0:
  283.             is_mime = 1
  284.         if cte.match(line) >= 0:
  285.             has_cte = 1
  286.             if qp.match(line) >= 0:
  287.                 is_qp = 1
  288.             elif base64.match(line) >= 0:
  289.                 is_base64 = 1
  290.         if mp.match(line) >= 0:
  291.             multipart = '--' + mp.group(1)
  292.         if he.match(line) >= 0:
  293.             header_end = line
  294.             break
  295.         header.append(line)
  296.  
  297.     # read body
  298.     while 1:
  299.         line = ifile.readline()
  300.         if not line:
  301.             break
  302.         if multipart:
  303.             if line == multipart + '--\n':
  304.                 message_end = line
  305.                 break
  306.             if line == multipart + '\n':
  307.                 message_end = line
  308.                 break
  309.         if is_base64:
  310.             message.append(line)
  311.             continue
  312.         if is_qp:
  313.             while line[-2:] == '=\n':
  314.                 line = line[:-2]
  315.                 newline = ifile.readline()
  316.                 if newline[:len(QUOTE)] == QUOTE:
  317.                     newline = newline[len(QUOTE):]
  318.                 line = line + newline
  319.             line = mime_decode(line)
  320.         message.append(line)
  321.         if not has_iso_chars:
  322.             if iso_char.search(line) >= 0:
  323.                 has_iso_chars = must_quote_body = 1
  324.         if not must_quote_body:
  325.             if len(line) > MAXLEN:
  326.                 must_quote_body = 1
  327.  
  328.     # convert and output header and body
  329.     for line in header:
  330.         if must_quote_header:
  331.             line = mime_encode_header(line)
  332.         if chrset.match(line) >= 0:
  333.             if has_iso_chars:
  334.                 # change us-ascii into iso-8859-1
  335.                 if string.lower(chrset.group(2)) == 'us-ascii':
  336.                     line = chrset.group(1) + \
  337.                            CHARSET + chrset.group(3)
  338.             else:
  339.                 # change iso-8859-* into us-ascii
  340.                 line = chrset.group(1) + 'us-ascii' + chrset.group(3)
  341.         if has_cte and cte.match(line) >= 0:
  342.             line = 'Content-Transfer-Encoding: '
  343.             if is_base64:
  344.                 line = line + 'base64\n'
  345.             elif must_quote_body:
  346.                 line = line + 'quoted-printable\n'
  347.             else:
  348.                 line = line + '7bit\n'
  349.         ofile.write(line)
  350.     if (must_quote_header or must_quote_body) and not is_mime:
  351.         ofile.write('Mime-Version: 1.0\n')
  352.         ofile.write('Content-Type: text/plain; ')
  353.         if has_iso_chars:
  354.             ofile.write('charset="%s"\n' % CHARSET)
  355.         else:
  356.             ofile.write('charset="us-ascii"\n')
  357.     if must_quote_body and not has_cte:
  358.         ofile.write('Content-Transfer-Encoding: quoted-printable\n')
  359.     ofile.write(header_end)
  360.  
  361.     for line in message:
  362.         if must_quote_body:
  363.             line = mime_encode(line, 0)
  364.         ofile.write(line)
  365.     ofile.write(message_end)
  366.  
  367.     line = message_end
  368.     while multipart:
  369.         if line == multipart + '--\n':
  370.             return
  371.         if line == multipart + '\n':
  372.             nifile = File(ifile, multipart)
  373.             mimify_part(nifile, ofile, 1)
  374.             line = nifile.peek
  375.             ofile.write(line)
  376.             continue
  377.  
  378. def mimify(infile, outfile):
  379.     '''Convert 8bit parts of a MIME mail message to quoted-printable.'''
  380.     if type(infile) == type(''):
  381.         ifile = open(infile)
  382.         if type(outfile) == type('') and infile == outfile:
  383.             import os
  384.             d, f = os.path.split(infile)
  385.             os.rename(infile, os.path.join(d, ',' + f))
  386.     else:
  387.         ifile = infile
  388.     if type(outfile) == type(''):
  389.         ofile = open(outfile, 'w')
  390.     else:
  391.         ofile = outfile
  392.     nifile = File(ifile, None)
  393.     mimify_part(nifile, ofile, 0)
  394.     ofile.flush()
  395.  
  396. import sys
  397. if __name__ == '__main__' or (len(sys.argv) > 0 and sys.argv[0] == 'mimify'):
  398.     import getopt
  399.     usage = 'Usage: mimify [-l len] -[ed] [infile [outfile]]'
  400.  
  401.     opts, args = getopt.getopt(sys.argv[1:], 'l:ed')
  402.     if len(args) not in (0, 1, 2):
  403.         print usage
  404.         sys.exit(1)
  405.     if (('-e', '') in opts) == (('-d', '') in opts):
  406.         print usage
  407.         sys.exit(1)
  408.     for o, a in opts:
  409.         if o == '-e':
  410.             encode = mimify
  411.         elif o == '-d':
  412.             encode = unmimify
  413.         elif o == '-l':
  414.             try:
  415.                 MAXLEN = string.atoi(a)
  416.             except:
  417.                 print usage
  418.                 sys.exit(1)
  419.     if len(args) == 0:
  420.         encode(sys.stdin, sys.stdout)
  421.     elif len(args) == 1:
  422.         encode(args[0], sys.stdout)
  423.     else:
  424.         encode(args[0], args[1])
  425.